#include <dlfcn.h>
#include <cpu/cpu.h>
#include <memory/paddr.h>
#include <utils.h>
#include <difftest-def.h>
#include <myconfig.h>

typedef void (*REF_DIFFTEST_MEMCPY)(paddr_t, void*, size_t, bool);
typedef void (*REF_DIFFTEST_REGCPY)(void *, bool );
typedef void (*REF_DIFFTEST_EXEC)(uint64_t );
typedef void (*REF_DIFFTEST_RAISE_INTR)(uint64_t );
typedef void (*REF_DIFFTEST_INIT)(int);
void isa_reg_display();
REF_DIFFTEST_MEMCPY ref_difftest_memcpy = NULL;
REF_DIFFTEST_REGCPY ref_difftest_regcpy = NULL;
REF_DIFFTEST_EXEC ref_difftest_exec = NULL;
REF_DIFFTEST_RAISE_INTR ref_difftest_raise_intr= NULL;
REF_DIFFTEST_INIT ref_difftest_init= NULL;
uint64_t * cpu_gpr;


#ifdef DIFFTEST
static bool is_skip_ref = false;
static int skip_dut_nr_inst = 0;

bool isa_difftest_checkregs(riscv64_CPU_state *ref_r, vaddr_t pc) {
//    printf("IN DIFFTEST: ref_r->dnpc:%lx,npc.dnpc:%lx\n",ref_r->pc,pc);

	if (ref_r->pc != pc && ref_r->pc != 0x80000004){
		printf("\n\nPC FAULT!:ref_r->pc:%lx,cpu.pc:%lx\n\n",ref_r->pc,pc);
//      		for (int i=0;i<32;i++){
//	          printf("ref_r->gpr[%d]:0x%016lx,cpu.gpr[%d]:0x%016lx \n",i,ref_r->gpr[i],i,cpu_gpr[i]);
//	  	}

  		return false;
//	      return true;
      }
      else{
      	for (int i=0;i<32;i++){
	  if(ref_r->gpr[i] != cpu_gpr[i]) 
	    {
	    printf("ref_r->dnpc:%lx,npc.dnpc:%lx\n",ref_r->pc,pc);
	    printf("ref_r->gpr[%d]:0x%016lx,cpu.gpr[%d]:0x%016lx \n",i,ref_r->gpr[i],i,cpu_gpr[i]);
	    return false;
	  }
	}
	return true;
      }
}


// this is used to let ref skip instructions which
// can not produce consistent behavior with NEMU
void difftest_skip_ref() {
  is_skip_ref = true;
  // If such an instruction is one of the instruction packing in QEMU
  // (see below), we end the process of catching up with QEMU's pc to
  // keep the consistent behavior in our best.
  // Note that this is still not perfect: if the packed instructions
  // already write some memory, and the incoming instruction in NEMU
  // will load that memory, we will encounter false negative. But such
  // situation is infrequent.
  skip_dut_nr_inst = 0;
}

// this is used to deal with instruction packing in QEMU.
// Sometimes letting QEMU step once will execute multiple instructions.
// We should skip checking until NEMU's pc catches up with QEMU's pc.
// The semantic is
//   Let REF run `nr_ref` instructions first.
//   We expect that DUT will catch up with REF within `nr_dut` instructions.
void difftest_skip_dut(int nr_ref, int nr_dut) {
  skip_dut_nr_inst += nr_dut;

  while (nr_ref -- > 0) {
    ref_difftest_exec(1);
  }
}

void init_difftest(char *ref_so_file, long img_size, int port) {

  assert(ref_so_file != NULL);

  void *handle;
  handle = dlopen(ref_so_file, RTLD_LAZY | MUXNDEF(CONFIG_CC_ASAN, RTLD_DEEPBIND, 0));
  assert(handle);

  ref_difftest_memcpy = (REF_DIFFTEST_MEMCPY)dlsym(handle, "difftest_memcpy");
  assert(ref_difftest_memcpy);

  ref_difftest_regcpy = (REF_DIFFTEST_REGCPY) dlsym(handle, "difftest_regcpy");
  assert(ref_difftest_regcpy);

  ref_difftest_exec = (REF_DIFFTEST_EXEC) dlsym(handle, "difftest_exec");
  assert(ref_difftest_exec);

  ref_difftest_raise_intr = (REF_DIFFTEST_RAISE_INTR) dlsym(handle, "difftest_raise_intr");
  assert(ref_difftest_raise_intr);

  ref_difftest_init = (REF_DIFFTEST_INIT) dlsym(handle, "difftest_init");
  assert(ref_difftest_init);
  
  Log("Differential testing: %s", MUXDEF(DIFFTEST, ASNI_FMT("ON", ASNI_FG_GREEN), ASNI_FMT("OFF", ASNI_FG_RED)));
  Log("The result of every instruction will be compared with %s. "
      "This will help you a lot for debugging, but also significantly reduce the performance. "
      "If it is not necessary, you can turn it off in menuconfig.", ref_so_file);

  ref_difftest_init(port);
  ref_difftest_memcpy(RESET_VECTOR, guest_to_host(RESET_VECTOR), img_size, DIFFTEST_TO_REF);
//  Log("RESET vector:%x,img_size:%ld",RESET_VECTOR,img_size);
  ref_difftest_regcpy(&cpu, DIFFTEST_TO_REF);

}

static void checkregs(riscv64_CPU_state *ref, vaddr_t pc) {

   if (!isa_difftest_checkregs(ref,pc)) {
    npc_state.state = NEMU_ABORT;
    npc_state.halt_pc = pc;
    isa_reg_display();
//    for (int i=0;i<32;i++)
    Log("can not catch up with ref.pc = " FMT_WORD "\n" ,ref->pc);
  }
}

void difftest_step(vaddr_t pc, vaddr_t npc) {
  riscv64_CPU_state ref_r;

  if (skip_dut_nr_inst > 0) {
    ref_difftest_regcpy(&ref_r, DIFFTEST_TO_DUT);
    if (ref_r.pc == npc) {
      skip_dut_nr_inst = 0;
      checkregs(&ref_r, npc);
      return;
    }
    skip_dut_nr_inst --;
    if (skip_dut_nr_inst == 0)
      panic("can not catch up with ref.pc = " FMT_WORD " at pc = " FMT_WORD, ref_r.pc, pc);
    return;
  }

  if (is_skip_ref) {
    // to skip the checking of an instruction, just copy the reg state to reference design
    for(int i = 0; i<32 ; i++)cpu.gpr[i] = cpu_gpr[i];
    cpu.pc = npc;
    ref_difftest_regcpy(&cpu, DIFFTEST_TO_REF);
    is_skip_ref = false;
    return;
  }

    ref_difftest_exec(1);
    ref_difftest_regcpy(&ref_r, DIFFTEST_TO_DUT);
    checkregs(&ref_r, npc);


}
#else
void init_difftest(char *ref_so_file, long img_size, int port) {}
#endif
